package edu.washington.cs.sounddetector.server.api;

import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.deser.std.StdKeyDeserializer;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.map.ser.std.StdKeySerializer;

import edu.washington.cs.sounddetector.server.api.method.Classify;
import edu.washington.cs.sounddetector.server.api.method.GetOldNotifications;
import edu.washington.cs.sounddetector.server.api.method.GetPendingNotifications;
import edu.washington.cs.sounddetector.server.api.method.GetSound;
import edu.washington.cs.sounddetector.server.api.method.GetSounds;
import edu.washington.cs.sounddetector.server.api.method.RegisterReceiverPhone;
import edu.washington.cs.sounddetector.server.api.method.RenameDetector;
import edu.washington.cs.sounddetector.server.api.method.SetSoundClassProperties;
import edu.washington.cs.sounddetector.server.api.method.Train;
import edu.washington.cs.sounddetector.server.api.method.UnregisterReceiverPhone;
import edu.washington.cs.sounddetector.server.api.method.Untrain;
import edu.washington.cs.sounddetector.server.api.object.SoundClass;

/**
 * <p>
 * MethodConverter handles the serialization and deserialization of Method and MethodResult objects.
 * It also provides a way to retrieve the class of a method implementation given its name.
 * </p>
 * 
 * <p>
 * MethodConverter is immutable and threadsafe.
 * </p>
 * 
 * @author the.dylan.price@gmail.com
 */
public class MethodConverter
{
    /**
     * The Json serializer.
     */
    private ObjectMapper mapper;

    /**
     * Map from method name to Method class
     */
    private final Map<String, Class<? extends Method>> commandMap;

    /**
     * Constructs a new MethodConverter.
     */
    private MethodConverter()
    {
        mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule("SoundClassDeserializer", new Version(1, 0, 0, null));
        module.addKeySerializer(SoundClass.class, new SoundClassSerializer());
        module.addKeyDeserializer(SoundClass.class, new SoundClassDeserializer());
        mapper.registerModule(module);

        commandMap = new HashMap<String, Class<? extends Method>>();

        addMethod(RegisterReceiverPhone.class);
        addMethod(UnregisterReceiverPhone.class);

        addMethod(Train.class);
        addMethod(Untrain.class);

        addMethod(Classify.class);

        addMethod(GetPendingNotifications.class);
        addMethod(GetOldNotifications.class);

        addMethod(GetSound.class);
        addMethod(GetSounds.class);

        addMethod(SetSoundClassProperties.class);
        
        addMethod(RenameDetector.class);
    }

    private void addMethod(Class<? extends Method> method)
    {
        commandMap.put(StringUtils.uncapitalize(method.getSimpleName()), method);
    }

    /**
     * Converts the given command into its serialized form which can be sent over the wire.
     * 
     * @param command
     *            the command to serialize.
     * @return the serialized form of command.
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonGenerationException
     */
    public String serializeMethod(Method command) throws JsonGenerationException,
            JsonMappingException, IOException
    {
        return mapper.writeValueAsString(command);
    }

    /**
     * Converts the given result into its serialized form which can be sent over the wire.
     * 
     * @param result
     *            the result to serialize
     * @return the serialized form of the result
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonGenerationException
     */
    public <T extends Method> String serializeResult(MethodResult<T> result)
            throws JsonGenerationException, JsonMappingException, IOException
    {
        return mapper.writeValueAsString(result);
    }

    /**
     * Deserializes from contents of the given reader into a Method of the given type. Reads from
     * the reader until the Method can be parsed, but does not close it.
     * 
     * @param reader
     *            a Reader ready to stream the serialized form of a Method. The reader will still be
     *            open after the call.
     * @param type
     *            the type of Method to deserialize the contents of the reader into.
     * @return a Method of class 'type', representing the deserialized contents of the characters
     *         read from the reader.
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonParseException
     */
    public <T extends Method> T deserializeMethod(Reader reader, Class<T> type)
            throws JsonParseException, JsonMappingException, IOException
    {
        return mapper.readValue(reader, type);
    }

    /**
     * Deserializes the contents of the given String into a Method of the given type.
     * 
     * @param serialized
     *            a String containing the serialized form of a Method.
     * @param type
     *            the type of Method to deserialize the contents of serialized into.
     * @return a Method of class 'type', representing the deserialized contents of serialized.
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonParseException
     */
    public <T extends Method> T deserializeMethod(String serialized, Class<T> type)
            throws JsonParseException, JsonMappingException, IOException
    {
        return mapper.readValue(serialized, type);
    }

    /**
     * Deserializes from the contents of the given reader into a MethodResult of the given type.
     * Reads from the reader until the Method can be parsed but does not close it.
     * 
     * @param reader
     *            a Reader ready to stream the serialized form of a MethodResult. The reader will
     *            still be open after the call.
     * @param type
     *            the type of MethodResult to deserialize the contents of the reader into.
     * @return a MethodResult of class 'type', representing the deserialized contents of characters
     *         read from the reader.
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonParseException
     */
    public <T extends MethodResult<?>> T deserializeResult(Reader reader, Class<T> type)
            throws JsonParseException, JsonMappingException, IOException
    {
        return mapper.readValue(reader, type);
    }

    /**
     * Deserializes the contents of the given String into a MethodResult of the given type.
     * 
     * @param serialized
     *            a String containing the serialized form of a MethodResult
     * @param type
     *            the type of MethodResult to deserialize the String into
     * @return a MethodResult of class 'type' representing the deserialized contents of the string
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonParseException
     */
    public <T extends MethodResult<?>> T deserializeResult(String serialized, Class<T> type)
            throws JsonParseException, JsonMappingException, IOException
    {
        return mapper.readValue(serialized, type);
    }

    /**
     * Returns the class implementing the Method interface which corresponds to the given
     * methodName.
     * 
     * @param methodName
     *            the name of a method. Returns null if no method with the given methodName exists.
     * @return the class of an implementation of the Method interface which corresponds to the given
     *         methodName.
     */
    public Class<? extends Method> getMethodClass(String methodName)
    {
        return commandMap.get(methodName);
    }

    /**
     * This ensures that the singletons are instantiated once and in a threadsafe way. See
     * http://en.wikipedia.org/wiki/Singleton_pattern#The_solution_of_Bill_Pugh
     */
    private static class SingletonHolder
    {
        public static final MethodConverter instance = new MethodConverter();
    }

    /**
     * @return the singleton MethodConverter instance.
     */
    public static MethodConverter getInstance()
    {
        return SingletonHolder.instance;
    }

    private class SoundClassDeserializer extends StdKeyDeserializer
    {

        public SoundClassDeserializer()
        {
            super(SoundClass.class);
        }

        protected Object _parse(String key, DeserializationContext ctxt) throws Exception
        {
            return mapper.readValue(key, SoundClass.class);
        }
    }

    private class SoundClassSerializer extends StdKeySerializer
    {
        public SoundClassSerializer()
        {
            super();
        }

        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider)
                throws IOException, JsonGenerationException
        {
            String json = mapper.writeValueAsString(value);
            jgen.writeFieldName(json);
        }
    }
}
